﻿using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;

using BrightSword.Feber.Core;
using BrightSword.SwissKnife;

namespace BrightSword.Feber.Samples
{
    public static class CloneFactory
    {
        /// <summary>
        ///   Creates a new instance of <typeparamref name="TSource" />, with all properties shallow-copied from a given instance of the same type
        /// </summary>
        /// <param name="source"> The source from which to map values </param>
        /// <typeparam name="TSource"> A type of which <paramref name="source" /> is an instance </typeparam>
        public static TSource Clone<TSource>(this TSource source) where TSource : new()
        {
            return CloneFactory<TSource, TSource, TSource>.Clone(source);
        }

        /// <summary>
        ///   Creates a new instance of <typeparamref name="TDestination" />
        ///     with all properties shallow-copied from a given instance of <typeparamref name="TSource"/>
        ///   Gracefully ignores properties that do not exist in <typeparamref name="TDestination"/>
        ///     and defaults those properties in <typeparamref name="TDestination"/> not present in <typeparamref name="TSource"/>
        /// </summary>
        /// <param name="source"> The source from which to map values </param>
        /// <typeparam name="TSource"> A type of which <paramref name="source" /> is an instance </typeparam>
        /// <typeparam name="TDestination"> The type of the desired result </typeparam>
        public static TDestination Clone<TSource, TDestination>(this TSource source) where TDestination : new()
        {
            return CloneFactory<TSource, TSource, TDestination>.Clone(source);
        }

        /// <summary>
        ///   Creates a new instance of <typeparamref name="TDestination" /> 
        ///     with all properties defined in <typeparamref name="TBase"/> 
        ///     shallow-copied from a given instance of <typeparamref name="TSource"/>
        ///   Gracefully ignores properties that do not exist in <typeparamref name="TDestination"/>, 
        ///     and defaults those properties in <typeparamref name="TDestination"/> not present in <typeparamref name="TBase"/> or <typeparamref name="TSource"/>
        /// </summary>
        /// <param name="source"> The source from which to map values </param>
        /// <typeparam name="TSource"> A type which <paramref name="source" /> is an instance </typeparam>
        /// <typeparam name="TDestination"> The type of the desired result </typeparam>
        /// <typeparam name="TBase">The type which provides the set of properties to copy</typeparam>
        public static TDestination Clone<TSource, TDestination, TBase>(this TSource source) where TDestination : new()
        {
            return CloneFactory<TBase, TSource, TDestination>.Clone(source);
        }
    }

    public static class CloneFactory<TProto, TSource, TDestination>
        where TDestination : new()
    {
        private static readonly CloneFactoryBuilder builder = new CloneFactoryBuilder();
        
        public static TDestination Clone(TSource source)
        {
            var destination = new TDestination();

            builder.Action(destination,
                           source);

            return destination;
        }

        private class CloneFactoryBuilder : ActionBuilder<TProto, TDestination, TSource>
        {
            // ReSharper disable StaticFieldInGenericType
            private static readonly IEnumerable<PropertyInfo> sourceProperties = typeof (TSource).GetAllProperties();
            private static readonly IEnumerable<PropertyInfo> destinationProperties = typeof (TDestination).GetAllProperties();
            // ReSharper restore StaticFieldInGenericType

            protected override Expression PropertyExpression(PropertyInfo property,
                                                             ParameterExpression leftInstanceParameterExpression,
                                                             ParameterExpression rightInstanceParameterExpression)
            {
                var setPropertyInfo = MatchProperty(destinationProperties,
                                                    property);
                var getPropertyInfo = MatchProperty(sourceProperties,
                                                    property);

                if ((setPropertyInfo == null) || (setPropertyInfo.GetSetMethod() == null))
                {
                    return Expression.Default(typeof (void));
                }

                if ((getPropertyInfo == null) || (getPropertyInfo.GetGetMethod() == null))
                {
                    return Expression.Default(typeof (void));
                }

                return Expression.Assign(Expression.Property(leftInstanceParameterExpression,
                                                             setPropertyInfo),
                                         Expression.Property(rightInstanceParameterExpression,
                                                             getPropertyInfo));
            }

            private static PropertyInfo MatchProperty(IEnumerable<PropertyInfo> properties,
                                                      PropertyInfo property)
            {
                return properties.FirstOrDefault(_ => _.Name.Equals(property.Name) && _.PropertyType == property.PropertyType);
            }
        }
    }
}